Skip to content

Conversation

@DrMichaelPetter
Copy link
Collaborator

Closes #1494 by providing a domain lifter, that applies meet until the gas depletes. In a first application, we get a new parameter ana.apron.narrowing_gas to specify the amount of gas per state, which comes in handy for poyhedra-based analysis.

// SKIP PARAM: --set ana.apron.narrowing_gas 1 --set ana.activated[+] apron --set ana.apron.domain polyhedra
int loop(){
  int i, j, k;
  int a = 0;
  for (i = 500; i >= 1; i--)
  {
    a++;
    __goblint_check(a + i - 501 == 0); // needs 1x narrowing or octagons
  }
  return 0;
}

@DrMichaelPetter DrMichaelPetter self-assigned this Nov 21, 2025
@DrMichaelPetter DrMichaelPetter added feature precision relational Relational analyses (Apron, affeq, lin2var) labels Nov 21, 2025
@sim642 sim642 self-requested a review November 21, 2025 10:09
Comment on lines +18 to +36
let narrowing_gas = GobConfig.get_int "ana.apron.narrowing_gas"
in
if (narrowing_gas > 0) then
let module Narrowed =
struct
module PolyhedraChainParams: Printable.ChainParams =
struct
let n () = narrowing_gas
let names = string_of_int
end
include NarrowingGas.DLifter (Spec) (PolyhedraChainParams)
module A = Spec.A
let access = Spec.access
let name = Spec.name
end
in
(module Narrowed)
else
(module Spec)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in a slightly less ideal place than I imagined in #1494, but I can see why and it is fine.

Ideally, I'd like to just wrap this lifting around polyhedra, but not other Apron domains, somewhere in ApronDomain, and have the analysis be completely oblivious to the fact.
Because in some sense it's "just" a lifting of any Lattice.S → Lattice.S. But that's only so if things are fully properly abstract, but our RelationDomain/ApronDomain setup is far from that. So it'd require a large cleanup/refactoring to be able to do it in my dream way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's is purely a lattice -> lattice functor, as the gas needs to be reset at every transfer function, no?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the NarrowingGas.Dom functor here (based on WideningDelay, etc) is essentially just that. It's the act of unlift and lift to do the operation that leads to the reset. And that's what NarrowingGas.DLifter used here exactly does.

It additionally needs all relational domain operations (instead of Spec operations) lifted, which needs more boilerplate. And it won't really gain anything from Goblint usage perspective.

@DrMichaelPetter
Copy link
Collaborator Author

DrMichaelPetter commented Nov 21, 2025

There is some strange influence between apron w. narrow gas 1 and def_exc going on, when the loopUnrollHeuristics is on:

//PARAMS:  --set ana.apron.narrowing_gas 1 --set ana.activated[+] apron --set ana.apron.domain polyhedra --set ana.autotune.enabled true --set ana.autotune.activated '["loopUnrollHeuristic"]' 
int main() {
    int x;
    int y;
    int xtmp;
    if (y >= 0) 
    {
        xtmp = x;
        while(xtmp>y) {
            xtmp = xtmp - y;
        }
        y = xtmp;
    }
    return 0;
}

Goblint does not reach a fixpoint in this configuration, with a diff for
xtmp = (Unknown int([-31,31])) instead of (Not {0}([0,31])) at the inner loop guard.

@sim642
Copy link
Member

sim642 commented Nov 21, 2025

With --trace sol --trace sol2 --trace solchange you can see the last iteration (immediately after the narrow by meet) of that unknown:

%%% sol: Var: L:node 27 "xtmp > y" on narrowing-gas-fp-error.c:9:15-9:21 (wp: true)
Old value: HConsed lifted PathSensitive (ProjectiveSet (MCP.D * map)):PathSensitive (ProjectiveSet (MCP.D * map)):{(MCP.D:[expRelation:(),
                                                                                                                           mallocWrapper:(wrapper call:Unknown node, unique calls:{}),
                                                                                                                           base:({
                                                                                                                                   Local {
                                                                                                                                     x ->   ⊤
                                                                                                                                     y ->   (Unknown int([0,31]))
                                                                                                                                     xtmp ->   (Not {0}([0,31]))
                                                                                                                                   }
                                                                                                                                 }, {}, {}, {}),
                                                                                                                           threadid:(wrapper call:unknown node, Thread:[main], created:(current function:bot, callees:bot)),
                                                                                                                           threadflag:Singlethreaded,
                                                                                                                           threadreturn:true,
                                                                                                                           escape:{},
                                                                                                                           mutexEvents:(),
                                                                                                                           access:(),
                                                                                                                           mutex:(lockset:{}, multiplicity:{}),
                                                                                                                           race:(),
                                                                                                                           mhp:(),
                                                                                                                           assert:(),
                                                                                                                           pthreadMutexType:(),
                                                                                                                           apron:(Apron Polyhedra * Unit:([|-x#297+2147483647>=0; -xtmp#299+2147483647>=0; xtmp#299-1>=0; xtmp#299+y#298-1>=0|] (env: [|0> x#297:int; 1> xtmp#299:int; 2> y#298:int|]), ()), chain:1)], map:{})}
Eqd: HConsed lifted PathSensitive (ProjectiveSet (MCP.D * map)):PathSensitive (ProjectiveSet (MCP.D * map)):{(MCP.D:[expRelation:(),
                                                                                                                     mallocWrapper:(wrapper call:Unknown node, unique calls:{}),
                                                                                                                     base:({
                                                                                                                             Local {
                                                                                                                               x ->   ⊤
                                                                                                                               y ->   (Unknown int([0,31]))
                                                                                                                               xtmp ->   ⊤
                                                                                                                             }
                                                                                                                           }, {}, {}, {}),
                                                                                                                     threadid:(wrapper call:unknown node, Thread:[main], created:(current function:bot, callees:bot)),
                                                                                                                     threadflag:Singlethreaded,
                                                                                                                     threadreturn:true,
                                                                                                                     escape:{},
                                                                                                                     mutexEvents:(),
                                                                                                                     access:(),
                                                                                                                     mutex:(lockset:{}, multiplicity:{}),
                                                                                                                     race:(),
                                                                                                                     mhp:(),
                                                                                                                     assert:(),
                                                                                                                     pthreadMutexType:(),
                                                                                                                     apron:(Apron Polyhedra * Unit:([|-x#297+2147483647>=0; -xtmp#299-y#298+2147483647>=0; -xtmp#299+2147483647>=0; xtmp#299-1>=0; xtmp#299+2y#298-1>=0|] (env: [|0> x#297:int; 1> xtmp#299:int; 2> y#298:int|]), ()), chain:0)], map:{})}

For some reason the right-hand side for the loop head is not monotonic? Or somehow with improved apron abstraction, there's less precise xtmp in base when reevaluated.

In particular, with more tracing, Apron doesn't seem to return a lower bound 1 which it seems to have:

        %%% evalint: (narrowing-gas-fp-error.c:10:13-10:28)  relation query xtmp - y (MinusA(Lval(Var(xtmp, NoOffset)), Lval(Var(y, NoOffset))))
          %%% evalint: relation st: ([|-x#297+2147483647>=0; -xtmp#299+2147483647>=0; xtmp#299-y#298-1>=0; xtmp#299+y#298-1>=0|] (env: [|0> x#297:int; 1> xtmp#299:int; 2> y#298:int|]), ())
          %%% relation: eval_int: exp_is_constraint MinusA(Lval(Var(xtmp, NoOffset)), Lval(Var(y, NoOffset))) = false
          %%% relation: texpr1_expr_of_cil_exp/simplify: Lval(Var(y, NoOffset)) -> (Unknown int([0,31]))
          %%% relation: texpr1_expr_of_cil_exp/simplify: Lval(Var(xtmp, NoOffset)) -> (Not {0}([0,31]))
          %%% relation: texpr1_expr_of_cil_exp: MinusA(Lval(Var(xtmp, NoOffset)), Lval(Var(y, NoOffset))) -> xtmp#299 -_i,n y#298 (true)
          %%% evalint: relation query xtmp - y -> IntDomLifter(intdomtuple):⊤

@DrMichaelPetter
Copy link
Collaborator Author

Here is a more simplified version of the same thing without autotuner -- so definitely a pure apron/polyhedra and narrowing thing:

// SKIP PARAM --set ana.apron.narrowing_gas 1 --set ana.activated[+] apron --set ana.apron.domain polyhedra
int main(void)
{
  int x;
  int y;
  int xtmp;
  if (y < 0) return 0;
  if (x <= y) return 0;
  xtmp = x-y;
  while (xtmp > y)
  {
      xtmp -= y;
  }
  return 0;
}

@DrMichaelPetter
Copy link
Collaborator Author

In particular, with more tracing, Apron doesn't seem to return a lower bound 1 which it seems to have:

        %%% evalint: (narrowing-gas-fp-error.c:10:13-10:28)  relation query xtmp - y (MinusA(Lval(Var(xtmp, NoOffset)), Lval(Var(y, NoOffset))))
          %%% evalint: relation st: ([|-x#297+2147483647>=0; -xtmp#299+2147483647>=0; xtmp#299-y#298-1>=0; xtmp#299+y#298-1>=0|] (env: [|0> x#297:int; 1> xtmp#299:int; 2> y#298:int|]), ())
          %%% relation: eval_int: exp_is_constraint MinusA(Lval(Var(xtmp, NoOffset)), Lval(Var(y, NoOffset))) = false
          %%% relation: texpr1_expr_of_cil_exp/simplify: Lval(Var(y, NoOffset)) -> (Unknown int([0,31]))
          %%% relation: texpr1_expr_of_cil_exp/simplify: Lval(Var(xtmp, NoOffset)) -> (Not {0}([0,31]))
          %%% relation: texpr1_expr_of_cil_exp: MinusA(Lval(Var(xtmp, NoOffset)), Lval(Var(y, NoOffset))) -> xtmp#299 -_i,n y#298 (true)
          %%% evalint: relation query xtmp - y -> IntDomLifter(intdomtuple):⊤

It seems to get lost during a sharedFunctions.eval_int while converting [1, 4294967293] by aprons bound_texpr into an interval via ID.of_interval

@sim642
Copy link
Member

sim642 commented Nov 22, 2025

The upper bound 4294967293 overflows int, which is probably why it goes to top. Maybe with sem.int.signed_overflow being assume_none it'd behave differently?

Either way, there's something strange because there should be a less precise state pre-narrowing, yet it somehow has a better upper bound to avoid overflowing to top.

@DrMichaelPetter
Copy link
Collaborator Author

The upper bound 4294967293 overflows int, which is probably why it goes to top. Maybe with sem.int.signed_overflow being assume_none it'd behave differently?

It behaves the same.

Either way, there's something strange because there should be a less precise state pre-narrowing, yet it somehow has a better upper bound to avoid overflowing to top.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements a "narrowing gas" feature to improve precision in polyhedra-based analysis by controlling when narrowing operations are applied through a counter-based mechanism. The feature helps achieve precise analysis results for nested loop scenarios that would otherwise require more expensive domains like octagons.

Changes:

  • Added a new domain lifter that pairs abstract elements with counters to delay narrowing operations
  • Introduced ana.apron.narrowing_gas configuration parameter to control narrowing delay per state
  • Added test case demonstrating improved precision for nested loops with polyhedra domain

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/lifters/narrowingGas.ml New lifter module implementing narrowing delay with counter-based gas mechanism
src/analyses/apron/apronAnalysis.apron.ml Integration of narrowing gas lifter into Apron analysis when narrowing_gas > 0
src/config/options.schema.json Added schema definition for ana.apron.narrowing_gas parameter with default value 0
tests/regression/89-apron3/04-polyhedra-narrowing1.c Test case verifying improved precision for nested loops requiring narrowing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

end


(** Lift {!S} to use widening delay for local states.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says "widening delay" but this module implements narrowing delay/gas, not widening delay. The comment should refer to "narrowing delay" or "narrowing gas" to match the actual functionality.

Copilot uses AI. Check for mistakes.
List.map (fun x -> (x, snd man.local)) @@ S.paths_as_set (conv man)
end

(** Lift {!S} to use widening delay for global unknowns. *)
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation says "widening delay" but this module implements narrowing delay/gas, not widening delay. The comment should refer to "narrowing delay" or "narrowing gas" to match the actual functionality.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +18
let n () = GobConfig.get_int "ana.narrowing.gas"
let names = string_of_int
end

module GlobalChainParams: Printable.ChainParams =
struct
let n () = GobConfig.get_int "ana.narrowing.gas"
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration key "ana.narrowing.gas" does not exist in the schema. Since LocalChainParams is not currently used in the codebase, this module references a non-existent configuration option. Consider either: 1) Adding "ana.narrowing.gas" to the schema if this is intended to be a general-purpose parameter, or 2) Removing LocalChainParams if it's not needed, since the actual usage in apronAnalysis.apron.ml creates a custom ChainParams with the apron-specific narrowing_gas value.

Suggested change
let n () = GobConfig.get_int "ana.narrowing.gas"
let names = string_of_int
end
module GlobalChainParams: Printable.ChainParams =
struct
let n () = GobConfig.get_int "ana.narrowing.gas"
(* Local default gas for narrowing; avoids reliance on a non-existent config key. *)
let default_gas = 3
let n () = default_gas
let names = string_of_int
end
module GlobalChainParams: Printable.ChainParams =
struct
(* Global default gas for narrowing; avoids reliance on a non-existent config key. *)
let default_gas = 3
let n () = default_gas

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +20
module GlobalChainParams: Printable.ChainParams =
struct
let n () = GobConfig.get_int "ana.narrowing.gas"
let names = string_of_int
end
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The configuration key "ana.narrowing.gas" does not exist in the schema. Since GlobalChainParams is not currently used in the codebase, this module references a non-existent configuration option. Consider either: 1) Adding "ana.narrowing.gas" to the schema if this is intended to be a general-purpose parameter, or 2) Removing GlobalChainParams if it's not needed, since the actual usage in apronAnalysis.apron.ml creates a custom ChainParams with the apron-specific narrowing_gas value.

Copilot uses AI. Check for mistakes.
let narrow (b1, i1) (b2, i2) =
let i' = max i1 i2 in
if i' < ChainParams.n () then
(Base.meet b1 b2, i' + 1) (* Stop narrowing when counter exceeds limit. *)
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is misleading. When the counter is less than n(), the code uses meet to delay narrowing. When the counter reaches or exceeds n(), actual narrowing begins. The comment should say something like "Use meet to delay narrowing until counter reaches limit" to accurately reflect the behavior.

Suggested change
(Base.meet b1 b2, i' + 1) (* Stop narrowing when counter exceeds limit. *)
(Base.meet b1 b2, i' + 1) (* Use meet to delay narrowing until counter reaches limit. *)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature precision relational Relational analyses (Apron, affeq, lin2var)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Narrowing by fixed number of meets

4 participants